package com.cougil.uoc.seghismed.cipher;

import iaik.asn1.ASN1Object;
import iaik.asn1.CodingException;
import iaik.asn1.DerCoder;
import iaik.asn1.structures.AlgorithmID;
import iaik.pkcs.PKCSException;
import iaik.pkcs.pkcs7.EncryptedContentInfo;
import iaik.pkcs.pkcs7.EnvelopedData;
import iaik.pkcs.pkcs7.RecipientInfo;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;

import org.apache.log4j.Logger;

import com.cougil.uoc.seghismed.utils.SeghismedException;
import com.cougil.uoc.seghismed.utils.SeghismedUtils;

/**
 * Classe que permet encapsular la lògica relacionada amb xifrar i desxifrar informació
 * emprant la clau privada i el conjunt de certificats per als destinataris associats
 * 
 * @author Ignacio Cougil Jares
 * @author <a href="mailto:jcastellar@uoc.edu">Jordi Castella-Roca</a>
 */
public class CipherManager {
	
	private static final Logger logger = Logger.getLogger(CipherManager.class);

	private byte[] cipheredText;
	private byte[] clearText;
	private X509Certificate[] x509Chain;
	private PrivateKey privateKey;
	//private PublicKey publicKey;

	private CipherManager() {
		super();
	}
	
	/**
	 * Constructor per defecte a emprar només per desxifrar informació emprant la clau
	 * privada del destinatari
	 * 
	 * @param privKey
	 *            Clau privada de l'usuari a emprar
	 */
	public CipherManager(PrivateKey privKey) {
		this.setPrivateKey(privKey);
		this.setCertificateChain(null);
	}

	/**
	 * Constructor per defecte a emprar tant per xifrar com per desxifrar informació
	 * emprant la clau privada i el conjunt de certificats per als destinataris informats
	 * 
	 * @param privKey
	 *            Clau privada de l'usuari a emprar
	 * @param certificates
	 *            Conjunt de certificats a emprar per als destinataris
	 */
	public CipherManager(PrivateKey privKey,
			X509Certificate[] certificates) {
		this.setPrivateKey(privKey);
		this.setCertificateChain(certificates);
	}
	
	/**
	 * Constructor per defecte a emprat tant per xifrar com per desxifrar informació
	 * emprant la clau privada i l'únic certificat per al destinatari informat
	 * 
	 * @param privKey
	 *            Clau privada de l'usuari a emprar
	 * @param certificates
	 *            Certificat a emprar per l'únic destinatari
	 */
	public CipherManager(PrivateKey privKey,
			X509Certificate certificat) {
		this(privKey, new X509Certificate[] { certificat }) ;
	}

	private void setPrivateKey(PrivateKey privKey) {
		privateKey = privKey;
	}

	// Optional!
	// public void setPublicKey(PublicKey pubKey) {
	//   	publicKey = pubKey;
	// }

	private void setCertificateChain(X509Certificate[] certChain) {
		x509Chain = certChain;
	}

	public byte[] xifrar(String clearText) {
		byte[] encrypted = null;
		byte[] clear = SeghismedUtils.getBytes(clearText);
		int bytes = clear.length;
		logger.debug("* Missatge a xifrar: [" + clearText + "] (" + bytes + " bytes)");
		String xifrat = null;
		if (clearText != null && bytes > 0) {
			encrypted = encrypt(clear);
			xifrat = new String(encrypted);
		}
		logger.debug("* Missatge xifrat: ["+xifrat+"] ("+SeghismedUtils.getBytes(xifrat).length+" bytes)");
		return encrypted;
	}
	
	private byte[] encrypt(byte[] in) {
		
		this.clearText = new byte[in.length];
		System.arraycopy(in, 0, this.clearText, 0, in.length);

		doEncrypt();
		
		return cipheredText;
	}

	public String desxifrar(byte[] cipherText) {
		String desxifrat = null;
		String xifrat = SeghismedUtils.getString(cipherText);
		byte[] bytesXifrats = SeghismedUtils.getBytes(xifrat);
		int bytes = bytesXifrats.length;
		logger.debug("* Missatge a desxifrar: [" + xifrat + "] (" + bytes + " bytes)");
		if (cipherText != null && bytes > 0) {
			byte[] decrypted = decrypt(cipherText);
			desxifrat = new String(decrypted);			
		}
		logger.debug("* Missatge desxifrat: ["+desxifrat+"] ("+SeghismedUtils.getBytes(desxifrat).length+" bytes)");
		return desxifrat;
	}
	
	private byte[] decrypt(byte[] in) {

		this.cipheredText = new byte[in.length];
		System.arraycopy(in, 0, this.cipheredText, 0, in.length);

		doDecrypt();
		
		return clearText;
	}

	private void doEncrypt() {
		try {
			int i;
	
			// use TripleDES in CBC mode for encrypting the content
			EnvelopedData enveloped_data = new EnvelopedData(clearText,
					AlgorithmID.des_EDE3_CBC);
	
			// For each intended recipient, create a RecipientInfo object, and add
			// all RecipientInfos to
			// the EnvelopedData structure by calling the setRecipientInfos method,
			// e.g. (assuming to add
			// two recipients with corresponding certificates cert1 and cert2;
			// currently only the PKCS#1
			// rsaEncryption is supported as key- encryption algorithm):
	
			RecipientInfo[] recipients = new RecipientInfo[x509Chain.length];
	
			for (i = 0; i < x509Chain.length; i++) {
				recipients[i] = new RecipientInfo(x509Chain[i], AlgorithmID.rsaEncryption);
			}
	
			enveloped_data.setRecipientInfos(recipients);
	
			// Prepare the EnvelopedData object for transmission by transforming it
			// into an ASN1Object
			// or immediately encoding it. This step also will perform the
			// encryption of the symmetric
			// content-encryption key for each participated recipient.
			// ASN1Object obj = enveloped_data.toASN1Object();
			// respectively
	
			cipheredText = enveloped_data.getEncoded();
		} catch (NoSuchAlgorithmException e) {
			throw new SeghismedException(e);
		} catch (PKCSException e) {
			throw new SeghismedException(e);
		} 
	}

	private void doDecrypt() {
		try {
			// If the EnvelopedData is supplied as DER encoding, first decode it to
			// an ASN1Object:
			ASN1Object obj = DerCoder.decode(cipheredText);
			// Create an EnvelopedData structure from the supplied ASN.1 object, and
			// parse the internal structure:
			EnvelopedData enveloped_data = new EnvelopedData(obj);
			// Get information about the inherent EncryptedContentInfo:
			EncryptedContentInfo eci = (EncryptedContentInfo) enveloped_data
					.getEncryptedContentInfo();

			// System.out.println("Content type: "+eci.getContentType().getName());
			// System.out.println("Content encryption algorithm:
			// "+eci.getContentEncryptionAlgorithm().getName());

			// Get information about the included RecipientInfos:

			RecipientInfo[] recipients = enveloped_data.getRecipientInfos();

			// System.out.pritnln("Included RecipientInfos:");
			//
			// for (int i=0; i < recipients.length; i++) {
			// System.out.print("Recipient "+(i+1)+":");
			// System.out.println(recipients[i].getIssuerAndSerialNumber());
			// }

			// Use some recipient s specific private key to decrypt the inherent
			// encrypted secret key
			// to be used for subsequently performing encrypted-content decryption:

			// setup cipher for recipient 1:
			int recipientInfoIndex = 0;
			enveloped_data.setupCipher(privateKey, recipientInfoIndex);

			// Unlike the stream supporting EnvelopedDataStream class where the
			// setupCipher method
			// only initializes the cipher for decryption, whole the
			// encrypted-content decryption
			// already is performed inside the setupCipher method of this class.

			// Get the recovered content:

			clearText = enveloped_data.getContent();

		} catch (CodingException e) {
			throw new SeghismedException(e);
		} catch (InvalidKeyException e) {
			throw new SeghismedException(e);
		} catch (NoSuchAlgorithmException e) {
			throw new SeghismedException(e);
		} catch (PKCSException e) {
			throw new SeghismedException(e);
		}
	}

}
